class Map {
	
	private var _tileMap:Array;
	private var _pathMap:Array;
	private var _highlightedTiles:Array;
	private var _unfoggedTiles:Array;
	
	private var _mouseOverTile:Tile;
	private var _selectedTile:Tile;
	private var UPHEIGHT_COST_INCREMENT = 2.0f;
	private var DOWNHEIGHT_COST_REDUCTION = 1.0f;
	
	//grid
	private var _grid:GameObject = null;
	
	function Map(tileMap:Array)
	{
		_tileMap = tileMap;
		for (var i =0; i < _tileMap.length; i++)
		{
			for (var j = 0; j < _tileMap[0].length; j++)
			{
				_tileMap[i][j].setMapRef(this);
			}
		}
		_highlightedTiles = new Array();
		_unfoggedTiles = null;
		_grid = MonoBehaviour.Instantiate(Resources.Load("Grid"));
		_mouseOverTile = _tileMap[0][0];
		_selectedTile = null;
	}
	
	function Map()
	{
		
	}
	
	function setMap(tileMap:Array)
	{
		_tileMap = tileMap;
		for (var i =0; i < _tileMap.length; i++)
		{
			for (var j = 0; j < _tileMap[0].length; j++)
			{
				_tileMap[i][j].setMapRef(this);
			}
		}
		_highlightedTiles = new Array();
		_unfoggedTiles = null;
		_grid = MonoBehaviour.Instantiate(Resources.Load("Grid"));
		_mouseOverTile = _tileMap[0][0];
		_selectedTile = null;
	}	
	
	//DOES only path finding. further processing and retrieval of data is done in another function
	//stores the map's movement direction in a 2d array called _pathMap
	public function checkRangeFrom(pos:Vector2, distance:int):void
	{
		//initialize the 2d array and set everything to null
		_pathMap = new Array(_tileMap.length);
		for (var i:int = 0; i < _pathMap.length; i++)
		{
			_pathMap[i] = new Array(_tileMap[0].length);
			for (var j:int = 0; j < _tileMap[0].length; j++)
				_pathMap[i][j] = null;
		}
		
		//open list used to do BFS
		var openList:Array = new Array();
		//add source to queue and set visited
		openList.Push(_tileMap[pos.x][pos.y]);
		//x,y = parent tile.  z = energy left (radius left)
		_pathMap[pos.x][pos.y] = new Vector3(-1,-1,distance);
		
		//BFS (modified. closer to djkstra's)
		while(openList.length != 0)
		{
			var currTile:Tile = openList.Shift();
			var currCoord:Vector2 = currTile.getCoord();
			var currPath:Vector3 = _pathMap[currCoord.x][currCoord.y];
			
			//If no "energy" to move, skip this tile
			if (currPath.z == 0)
				continue;
			
			//loop through all neighbours of current tile
			var currNeighbours:Array = currTile.getNeighbourTiles();
			for (i = 0; i < currNeighbours.length; i++)
			{
				//check whether there is a neighbour
				if (currNeighbours[i] == null)
					continue;
				var neighbour:Tile = currNeighbours[i];
				var nCoord:Vector2 = neighbour.getCoord();
				
				//"UNABLE TO MOVE" CONDITIONS
				//check if neighbour is an obstacle or occupied
				if (neighbour.isObstacle() || neighbour.isOccupied())
					continue;
				//if neighbour height is too high or low (2 height grades higher = can't move)
					//uphill check										//downhill check
				if (neighbour.getHeight() > currTile.getHeight()+1 || neighbour.getHeight() < currTile.getHeight()-1)
					continue;
				
				//CALCULATE COST TO MOVE TO THIS NEIGHBOUR
				var pathCost:float = neighbour.getCostToMove();
				//diagonal neighbours
				if (i == 1 || i == 3 || i == 5 || i == 7)
					pathCost += 0.41f; //according to pythagoras theorem
				//lower to higher ground
				if (currTile.getHeight() < neighbour.getHeight())
					pathCost += UPHEIGHT_COST_INCREMENT;//according to pythagoras theorem
				//higher to lower ground
				else if (currTile.getHeight() > neighbour.getHeight())
					pathCost -= DOWNHEIGHT_COST_REDUCTION;
			
				//if not enough "energy" left to move to this tile
				if (currPath.z < pathCost)
					continue;
					
				//if neighbour is unvisited || the current cost to move to this tile is lower
				if (_pathMap[nCoord.x][nCoord.y] == null || _pathMap[nCoord.x][nCoord.y].z < currPath.z - pathCost)
				{
					var path:Vector3 = new Vector3(currCoord.x, currCoord.y, currPath.z - pathCost);
					//set the path parent and cost left
					_pathMap[nCoord.x][nCoord.y] = path;
					//add to open list
					openList.Push(neighbour);
				}
			}
		}
	}

	//check whether path is reachable
	//MUST BE CALLED AFTER checkRangeFrom(...).
	public function checkReachable(pos:Vector2):boolean
	{
		//if path is not reachable
		if (_pathMap[pos.x][pos.y] == null)
			return false;
		//else if click on start point
		else if (_pathMap[pos.x][pos.y].x == -1 && _pathMap[pos.x][pos.y].y == -1)
			return false;
		else
			return true;
	}
	
	//get path to a tile on the map
	//MUST BE CALLED AFTER checkRangeFrom(...).
	public function getPath(pos:Vector2):Array
	{
		var path:Array = new Array();
		//add destination tile
		path.Push(_tileMap[pos.x][pos.y]);
		//get reverse trace for destination tile
		var currTile:Vector3 = _pathMap[pos.x][pos.y];
		while (true)
		{
			//trace the path map previously calculated 
			path.Unshift(_tileMap[currTile.x][currTile.y]);
			currTile = _pathMap[currTile.x][currTile.y];
			if (currTile.x == -1 && currTile.y == -1)
				break;
		}
		return path;
	}
	
	//get all tiles within the move radius
	//MUST BE CALLED AFTER checkRangeFrom(...).
	//pos: must be the start position previously calculated using checkRangeFrom(...)
	//radius: must be the radius previously calculated using checkRangeFrom(...)
	public function getMoveRadiusTiles(pos:Vector2, radius:int):Array
	{
		var tiles:Array = new Array();
		var initX:int = pos.x-radius;
		var initY:int = pos.y-radius;
		
		if(initX < 0) initX = 0;
		if(initY < 0) initY = 0;
		
		var xPos:int = initX;
		var yPos:int = initY;
		var diameter:int = radius+radius;
				
		for(var i:int=0; i<diameter; i++){
			
			for(var j:int=0; j<diameter; j++){			
				
				if(_pathMap[xPos][yPos] != null){
					tiles.Push(_tileMap[xPos][yPos]);
				}
				xPos ++;
				
				if(xPos >= _tileMap.length)
					break;
			}
			xPos = initX;
			yPos ++;
			
			if(yPos >= _tileMap[0].length)
				break;
		}	
		return tiles;			
	}
	
	//get all tiles within the move radius that is cover
	//MUST BE CALLED AFTER checkRangeFrom(...).
	//pos: must be the start position previously calculated using checkRangeFrom(...)
	//radius: must be the radius previously calculated using checkRangeFrom(...)
	public function getCoverTiles(pos:Vector2, radius:int){
		var tiles:Array = new Array();
		var initX:int = pos.x-radius;
		var initY:int = pos.y-radius;
		
		if(initX < 0) initX = 0;
		if(initY < 0) initY = 0;
		
		var xPos:int = initX;
		var yPos:int = initY;
		var diameter:int = radius+radius;
				
		for(var i:int=0; i<diameter; i++){
			
			for(var j:int=0; j<diameter; j++){			
				
				if(_pathMap[xPos][yPos] != null){
					if (_tileMap[xPos][yPos].isCover() && !_tileMap[xPos][yPos].isOccupied() && !_tileMap[xPos][yPos].isMovingOver()){
						tiles.Push(_tileMap[xPos][yPos]);
					}
				}
				xPos ++;
				
				if(xPos >= _tileMap.length)
					break;
			}
			xPos = initX;
			yPos ++;
			
			if(yPos >= _tileMap[0].length)
				break;
		}	

		return tiles;	
	}
		
	//highlight all tiles within move radius
	//MUST BE CALLED AFTER checkRangeFrom(...).
	//pos: must be the start position previously calculated using checkRangeFrom(...)
	//radius: must be the radius previously calculated using checkRangeFrom(...)
	public function highlightTiles(pos:Vector2, radius:int):void
	{
		var initX:int = pos.x-radius;
		var initY:int = pos.y-radius;
		
		if(initX < 0) initX = 0;
		if(initY < 0) initY = 0;
		
		var xPos:int = initX;
		var yPos:int = initY;
		var diameter:int = radius+radius;
				
		for(var i:int=0; i<diameter; i++){
			
			for(var j:int=0; j<diameter; j++){			
				
				if(_pathMap[xPos][yPos] != null){
					//highlight tile
					_tileMap[xPos][yPos].setTileState(Tile.STATE_HIGHLIGHT);
					//add to list for easy unhighlighting
					_highlightedTiles.Push(_tileMap[xPos][yPos]);
				}
				xPos ++;
				
				if(xPos >= _tileMap.length)
					break;
			}
			xPos = initX;
			yPos ++;
			
			if(yPos >= _tileMap[0].length)
				break;
		}						
	}
	
	//get all players/enemies in an area
	public function getTileCharacters(pos:Vector2, radius:int, isPlayer:boolean):Array
	{
		var characters:Array = new Array();
		var initX:int = pos.x-radius;
		var initY:int = pos.y-radius;
		
		if(initX < 0) initX = 0;
		if(initY < 0) initY = 0;
		
		var xPos:int = initX;
		var yPos:int = initY;
		var diameter:int = radius+radius;
				
		for(var i:int=0; i<diameter; i++){
			
			for(var j:int=0; j<diameter; j++){			
				
				var current_tile = _tileMap[xPos][yPos];
				
				if(isPlayer){
					if(current_tile.getCharacter() != null && current_tile.getCharacter().isVisibleToEnemy()){
						characters.Push(current_tile.getCharacter());
					}
				}else{
					if(current_tile.getEnemy() != null){
						characters.Push(current_tile.getEnemy());
					}
				}
				
				xPos ++;
				
				if(xPos >= _tileMap.length)
					break;
			}
			xPos = initX;
			yPos ++;
			
			if(yPos >= _tileMap[0].length)
				break;
		}	

		return characters;			
	} 
	
	//remove all highlighting of tiles
	public function unhighlightTiles():void
	{
		while(_highlightedTiles.length != 0)
		{
			var highlightedTile:Tile = _highlightedTiles.Shift();
			highlightedTile.setTileState(Tile.STATE_NORMAL);
		}
	}
	
	//initiate fog of war
	public function initiateFOW():void
	{
		for (var i:int = 0; i < _tileMap.length; i++)
			for (var j:int = 0; j < _tileMap[0].length; j++)
				_tileMap[i][j].setTileState(Tile.STATE_FOG);
	}
	//set fog of war
	//pass in a Vector3 of character position and their radius
	public function updateFog(characterPos:Array)
	{
		//initialize the unfoggedTiles
		//a 2d array storing all the tiles that is revealed by each character
		if (_unfoggedTiles == null)
		{
			_unfoggedTiles = new Array(characterPos.length);
			for (var index = 0; index < characterPos.length; index++)
				_unfoggedTiles[index] = new Array();
			//close the map with fog	
			initiateFOW();
		}
	
		for (var i:int = 0; i < characterPos.length; i++)
		{
			if(characterPos[i].getIsDead() && _unfoggedTiles[i].length != 0)
			{
				for (var wipeIndex = 0; wipeIndex < _unfoggedTiles[i].length; wipeIndex++)
				{
					_unfoggedTiles[i][wipeIndex].setTileState(Tile.STATE_FOG);
				}
				_unfoggedTiles[i] = new Array();
			}
			
			if(!characterPos[i].hasMoved())
				continue;
	
			//wipe the revealed tiles caused by this character
			//go through the 2d array to hide the tiles revealed by this character
			for (var unfoggedIndex = 0; unfoggedIndex < _unfoggedTiles[i].length; unfoggedIndex++)
			{
				_unfoggedTiles[i][unfoggedIndex].setTileState(Tile.STATE_FOG);
			}
			_unfoggedTiles[i] = new Array();
			//tag settled.
			characterPos[i].setHasMoved(false);
			var pos:Vector2 = characterPos[i].getCoord();
			var sRadius:int = characterPos[i].getSightRadius();
			//get tiles unfogged by this character
			_unfoggedTiles[i] = getUnfoggedTiles(pos, sRadius);
		}
	}
	
	private function getUnfoggedTiles(pos:Vector2, distance:int)
	{
		var unfoggedTiles = new Array();
		//initialize the 2d array and set everything to null
		var unfoggedMap = new Array(_tileMap.length);
		for (var i:int = 0; i < unfoggedMap.length; i++)
		{
			unfoggedMap[i] = new Array(_tileMap[0].length);
			for (var j:int = 0; j < _tileMap[0].length; j++)
				unfoggedMap[i][j] = null;
		}
		
		//open list used to do BFS
		var openList:Array = new Array();
		//add source to queue and set visited
		openList.Push(_tileMap[pos.x][pos.y]);
		//x,y = parent tile.  z = energy left (radius left)
		unfoggedMap[pos.x][pos.y] = new Vector3(-1,-1,distance);
		//unfog source tile
		_tileMap[pos.x][pos.y].setTileState(Tile.STATE_NORMAL);
		unfoggedTiles.Push(_tileMap[pos.x][pos.y]);
		
		//BFS (modified. closer to djkstra's)
		while(openList.length != 0)
		{
			var currTile:Tile = openList.Shift();
			var currCoord:Vector2 = currTile.getCoord();
			var currPath:Vector3 = unfoggedMap[currCoord.x][currCoord.y];
			
			//If no "energy" to move, skip this tile
			if (currPath.z == 0)
				continue;
			
			//loop through all neighbours of current tile
			var currNeighbours:Array = currTile.getNeighbourTiles();
			for (i = 0; i < currNeighbours.length; i++)
			{
				//check whether there is a neighbour
				if (currNeighbours[i] == null)
					continue;
				var neighbour:Tile = currNeighbours[i];
				var nCoord:Vector2 = neighbour.getCoord();
				
				//"UNABLE TO MOVE" CONDITIONS
				//if neighbour height is too high or low (2 height grades higher = can't move)
					//uphill check										//downhill check
				if (neighbour.getHeight() > currTile.getHeight()+1 || neighbour.getHeight() < currTile.getHeight()-1)
					continue;
				
				//CALCULATE COST TO MOVE TO THIS NEIGHBOUR
				var pathCost:float = neighbour.getCostToMove();
				//diagonal neighbours
				if (i == 1 || i == 3 || i == 5 || i == 7)
					pathCost += 0.41f; //according to pythagoras theorem
				//lower to higher ground
				if (currTile.getHeight() < neighbour.getHeight())
					pathCost += UPHEIGHT_COST_INCREMENT;//according to pythagoras theorem
				//higher to lower ground
				else if (currTile.getHeight() > neighbour.getHeight())
					pathCost -= DOWNHEIGHT_COST_REDUCTION;					
			
				//if not enough "energy" left to move to this tile
				if (currPath.z < pathCost)
					continue;
					
				//if neighbour is unvisited || the current cost to move to this tile is lower
				if (unfoggedMap[nCoord.x][nCoord.y] == null || unfoggedMap[nCoord.x][nCoord.y].z < currPath.z - pathCost)
				{
					//unfog only if visiting for the first time
					if (unfoggedMap[nCoord.x][nCoord.y] == null)
					{
						_tileMap[nCoord.x][nCoord.y].setTileState(Tile.STATE_NORMAL);
						unfoggedTiles.Push(_tileMap[nCoord.x][nCoord.y]);
					}
					
					var path:Vector3 = new Vector3(currCoord.x, currCoord.y, currPath.z - pathCost);
					//set the path parent and cost left
					unfoggedMap[nCoord.x][nCoord.y] = path;
					//add to open list
					openList.Push(neighbour);
				}
			}
		}
		return unfoggedTiles;
	}
	
	public function updateGrid()
	{
		_grid.transform.position = _mouseOverTile.getSurfaceCoord();
	}
	
	
	public function setMouseOverTile(tile:Tile)
	{
		_mouseOverTile = tile;
	}
	
	
	public function getMouseOverTile()
	{
		return _mouseOverTile;
	}
	
	public function setSelectedTile(tile:Tile)
	{
		_selectedTile = tile;
	}
	
	public function getSelectedTile()
	{
		return _selectedTile;
	}
	
	public function getAndClearSelectedTile()
	{
		var tempTile = _selectedTile;
		_selectedTile = null;
		return tempTile;
	}	

	public function getTile(x:int, z:int):Tile
	{
		return _tileMap[x][z];
	}
	
}
